package com.nightpiter.controller.algorithms;

import static com.nightpiter.controller.log.Logger.log;
import static com.nightpiter.controller.log.Logger.logProfilerBegin;
import static com.nightpiter.controller.log.Logger.logProfilerEnd;

import java.util.ArrayList;

import android.os.Handler;

import com.nightpiter.controller.algorithms.MetroRouterState.Action;
import com.nightpiter.controller.algorithms.exceptions.StationNotFoundDijkstraException;
import com.nightpiter.model.MetroModel;
import com.nightpiter.model.MetroModelData;
import com.nightpiter.model.MetroModelState;
import com.nightpiter.model.objects.Line;
import com.nightpiter.model.objects.Station;

/**
 * Dijkstra's shortest path algorithm
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroRouter {

	/**
	 * Router instance prepared to work with the model
	 */
	private static MetroRouter instance = null;

	/**
	 * the state
	 */
	MetroRouterState state;

	/**
	 * graph to search
	 */
	private Graph graph;

	/**
	 * start station from which begins search
	 */
	private Station fromStation;

	/**
	 * sorted list implementation instance to store not yet visited vertices
	 */
	private SortedVertexList unvisiteds;

	/**
	 * sorted list to store the next pass unvisited vertices
	 */
	private SortedVertexList nextPassUnvisiteds;

	/**
	 * @return MetroRouter instance prepared to work with the current model
	 */
	public static MetroRouter getInstance() {
		return instance;
	}

	/**
	 * private constructor
	 * 
	 * @param modelData
	 */
	private MetroRouter(MetroModelData modelData) {
		this.graph = new Graph(modelData);
		this.state = new MetroRouterState();
	}

	/**
	 * This method performs searching of the shortest ways between stationFrom
	 * and all others by using Dijkstra's algorithm
	 * 
	 * @param fromStation
	 *            the station from which begins search
	 */
	public void prepare(Station fromStation, Handler handler) {

		if (fromStation == null) {
			log("fromStation == null");
			this.state.action = Action.IDLE;
			state.setError(new StationNotFoundDijkstraException());
			return;
		} else if (this.fromStation == fromStation) {
			log("router has already prepared for this station");
			this.state.action = Action.IDLE;
			this.state.setReady(true);
			return;
		}

		this.state.action = Action.PREPARING;
		this.state.resetAll(handler);

		nextPassUnvisiteds = new SortedVertexList(0);

		logProfilerBegin("Starting new Dijkstra's calculation");
		log("reset each vertex");
		this.graph.reset();

		for (Line line : fromStation.getLines()) {
			Vertex fromVertex = graph.getVerticesHash().get(fromStation, line);
			if (fromVertex != null) {
				fromVertex.setAsStart();
				nextPassUnvisiteds.add(fromVertex);
				Way currentPassWay = fromVertex.passWayMap.get(0);
				log("adding vertex (total ways: "
						+ fromVertex.passWayMap.size()
						+ ", station "
						+ fromVertex.getStation()
						+ ", reach time: "
						+ (currentPassWay.transferDistance + currentPassWay.tunnelDistance)
						+ ")");
			} else {
				this.state.action = Action.IDLE;
				state.setError(new StationNotFoundDijkstraException());
				logProfilerEnd("Dijkstra's algorithm exited with an error");
				return;
			}
		}

		log("Starting algorithm");

		do {
			unvisiteds = nextPassUnvisiteds;
			nextPassUnvisiteds = new SortedVertexList(unvisiteds.getPass() + 1);
			log("pass " + unvisiteds.getPass() + " (" + unvisiteds.size()
					+ " enter points)");

			this.state.setPass(unvisiteds.getPass());
			while (!unvisiteds.isEmpty()) {
				processVertex(unvisiteds.getFirst());
			}

		} while (!nextPassUnvisiteds.isEmpty());

		this.state.action = Action.IDLE;
		this.state.setReady(true);
		logProfilerEnd("Dijkstra's algorithm successfully completed");
	}

	/**
	 * executes the main step of Dijkstra's algorithm
	 * 
	 * Update distance and position in sorted list on those vertices which can
	 * be immediately reached from the current vertex. Mark the current vertex
	 * as visited.
	 * 
	 * @param vertex
	 *            the vertex to process
	 */
	private void processVertex(Vertex vertex) {
		// Way currentPassWay = vertex.passWayMap.get(unvisiteds.getPass());
		// logProfilerBegin("processing vertex (total ways: "
		// + vertex.passWayMap.size()
		// + ", station "
		// + vertex.getStation()
		// + ", reach time: "
		// + (currentPassWay.transferDistance + currentPassWay.tunnelDistance)
		// + ")");
		int pass = unvisiteds.getPass();
		vertex.setAsVisited(pass);
		for (Edge edge : vertex.getEdges()) {
			Vertex neighbor = edge.getToVertex();
			if (neighbor.isVisited(pass)) {
				continue;
			}
			neighbor.tryWay(edge, unvisiteds, nextPassUnvisiteds);
		}
		unvisiteds.remove(vertex);
		// logProfilerEnd("removing vertex from unvisiteds (new size: "
		// + unvisiteds.size() + ")");
	}

	/**
	 * get all possible routes to the station
	 * 
	 * @param toStation
	 * @return routes array
	 * @throws StationNotFoundDijkstraException
	 *             if the presented station does not belong to the model
	 */
	public Route[] getRoutes(Station toStation)
			throws StationNotFoundDijkstraException {

		Vertex destinationPseudoVertex = new Vertex(toStation);
		ArrayList<Vertex> destinationVertices = new ArrayList<Vertex>();

		for (Line line : toStation.getLines()) {
			Vertex destinationVertex = graph.getVerticesHash().get(toStation,
					line);

			if (destinationVertex != null) {
				log("Vertex for station " + toStation.getName() + " at line "
						+ line.getName() + " found");
				destinationVertices.add(destinationVertex);
			} else {
				log("presented station does not belong to the model: "
						+ toStation.getName() + " at " + line.getName());
				throw new StationNotFoundDijkstraException(toStation);
			}
		}

		for (int pass = 0; pass < nextPassUnvisiteds.getPass(); pass++) {

			for (Vertex destinationVertex : destinationVertices) {
				Way vertexWay = destinationVertex.passWayMap.get(pass);
				if (vertexWay != null) {
					destinationPseudoVertex.tryWay(new Edge(destinationVertex,
							destinationPseudoVertex), vertexWay);
				}
			}
		}

		Route[] routes = new Route[destinationPseudoVertex.passWayMap.size()];
		log("total ways: " + routes.length);
		int i = 0;
		for (Way way : destinationPseudoVertex.passWayMap.values()) {
			routes[i++] = new Route(way);
		}

		return routes;
	}

	/**
	 * @return the state
	 */
	public MetroRouterState getState() {
		return state;
	}

	public static void onModelStateChanged(MetroModel model,
			MetroModelState state) {
		MetroModelState currentState = model.getState();
		if (state.getAction() != MetroModelState.Action.IDLE) {
			instance = null;
		} else if (currentState.isReady()) {
			// TODO unexpected behavior if state is too out-of-date
			instance = new MetroRouter(model.getData());
		}

	}

}
